export enum EDispatchType { initial, reset, start, pause, stop, };
export type DispatchCommandType = {
  type: EDispatchType;
  payload?: any;
}

const GRID_SIZE = {
  width  : 30,
  height : 30,
}
class Grid {
  constructor(
    public x:number, 
    public y:number,
    private color:string,
  ) { }

  draw(context:any) {
    context.fillStyle = this.color;
    context.fillRect(
      this.x * GRID_SIZE.width,
      this.y * GRID_SIZE.height,
      GRID_SIZE.width,
      GRID_SIZE.height
    );
  }
  clear(context:any) {
    context.clearRect(
      this.x * GRID_SIZE.width,
      this.y * GRID_SIZE.height,
      GRID_SIZE.width,
      GRID_SIZE.height
    );
  }
}

export enum ESnakeMoveDirection { left, right, up, down };
type FnMovedHandlerType = (head?:Grid) => void;
class Snake {
  readonly color = 'hsl(120, 100%, 50%)';
  readonly speed = 300;  // ms
  private body: Grid[] = [];
  private direction: ESnakeMoveDirection;
  private isGrowing: boolean = false;
  private intervalID: any = 0;
  private movedHandler: FnMovedHandlerType = () => {};

  constructor(size: number, onMoved: FnMovedHandlerType) {
    this.direction = ESnakeMoveDirection.right;
    for (let i=0;i<size;i++) {
      this.extendHead(i,0);
    }
    this.movedHandler = onMoved;
  }

  extendHead (x:number, y:number, context?: any) {
    this.body.unshift(new Grid(x,y, this.color));
    // if (!!context) {
    //   this.body[0].draw(context);
    // }
  }
  trimTail (context:any) {
    if (!this.isGrowing) {
      const grid = this.body.pop();
      // grid?.clear(context);
    } else {
      this.isGrowing = false;
    }
  }
  draw(context:any) {
    this.body.forEach((grid: Grid) => grid.draw(context))
  }

  moveOneStep(context:any) {
    // console.log('Snake::moveOneStep', this.direction)
    // console.log('move step', this.direction, ESnakeMoveDirection.right)
    const [dx,dy] = 
      (this.direction === ESnakeMoveDirection.left)   ? [-1,  0] :
      (this.direction === ESnakeMoveDirection.right)  ? [ 1,  0] :
      (this.direction === ESnakeMoveDirection.up)     ? [ 0, -1] :
      (this.direction === ESnakeMoveDirection.down)   ? [ 0,  1] :
      [0, 0]
    ;
    if ((dx + dy) === 0) return;
    const x = this.body[0].x + dx;
    const y = this.body[0].y + dy;

    for (let item of this.body) {
      if (
        (item.x === x) && 
        (item.y === y)
      ) {
        this.movedHandler(new Grid(-1, -1, this.color));
        return;
      }
    }
    
    this.extendHead(x, y, context);
    this.trimTail(context);
    this.movedHandler(this.body[0]);
  }
  go (context:any) {
    this.intervalID = setInterval(() => {
      // console.log('interval callback')
      this.moveOneStep(context);
    }, this.speed);
    // console.log(this.intervalID)
  }
  stop () {
    if(!!this.intervalID) {
      clearInterval(this.intervalID);
    }
  }
  grow () {
    this.isGrowing = true;
  }
  setDirection(dir: ESnakeMoveDirection) {
    this.direction = dir;
  }
}

class Food {
  readonly color = 'yellow';
  public body: Grid[] = [];
  constructor(x:number, y:number) {
    this.body.push(new Grid(x,y,this.color));
  }

  draw(context:any) {
    this.body.forEach((grid: Grid) => grid.draw(context))
  }
}

export type TGameStatus = 'ready'|'moving'|'stopped'|'unknown';
type FnStatusUpdatedHandlerType = (status: TGameStatus) => void;
export class Game {
  readonly xMin = 0;
  readonly yMin = 0;
  readonly xMax = 15;
  readonly yMax = 15;

  private canvasRef: any;
  private context: any;

  private snake?: Snake;
  private food?: Food;
  private status: TGameStatus = 'unknown';
  private statusUpdatedHandler?: FnStatusUpdatedHandlerType;

  constructor (ref:any, ctx:any, onStatusUpdated:FnStatusUpdatedHandlerType) {
    this.canvasRef = ref;
    this.context = ctx;
    this.statusUpdatedHandler = onStatusUpdated;
    this.createSnake();
    this.createFood();
    this.updateStatus('ready');
  }

  updateStatus(status: TGameStatus) {
    this.status = status;
    this.statusUpdatedHandler?.(status);
  }

  clearAll () {
    this.canvasRef.current.width  = GRID_SIZE.width  * (this.xMax + 1);
    this.canvasRef.current.height = GRID_SIZE.height * (this.yMax + 1);
  }

  drawAll () {
    this.snake?.draw(this.context);
    this.food?.draw(this.context);
  }

  createSnake () {
    if (!!this.snake) delete this.snake;
    this.snake = new Snake(5, this.onSnakeMoved.bind(this));
  }
  createFood (x?:number, y?:number) {
    if (!!this.food) delete this.food;
    this.food = new Food(
      x ?? (Math.floor((this.xMax - this.xMin) * Math.random()) + this.xMin),
      y ?? (Math.floor((this.yMax - this.yMin) * Math.random()) + this.yMin),
    );
  }

  onSnakeMoved (head?: Grid) {
    // console.log('onmove', head)
    const {x, y} = head || {x: -1, y: -1};
    // console.log(this)  // bind(this)
    if (
      (x < this.xMin) || 
      (y < this.yMin) || 
      (x > this.xMax) || 
      (y > this.xMax)
    ) {
      // console.log(head, this)
      this.snake?.stop();
      this.statusUpdatedHandler?.('stopped');
      return;
    }
    // else
    if (
      (x === this.food?.body?.[0]?.x) &&
      (y === this.food?.body?.[0]?.y) 
    ) {
      this.createFood();
      this.snake?.grow();
    }

    this.clearAll();
    this.drawAll();
  }

  start() {
    this.snake?.go(this.context);
    this.updateStatus('moving');
  }
  stop() {
    this.snake?.stop();
    this.updateStatus('stopped');
  }
  reset() {
    this.stop();
    this.createSnake();
    this.createFood();
    this.clearAll();
    this.drawAll();
    this.updateStatus('ready');
  }
  setDirection(dir: ESnakeMoveDirection) {
    this.snake?.setDirection(dir);
  }
}
